iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
AI Engineering

從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理系列 第 9

讓資料離開記憶體:檔案讀寫與 Agent 的長期記憶

  • 分享至 

  • xImage
  •  

昨天我們讓程式在出錯時不會停下來。今天要解決另一個更根本的限制:

程式一關掉,所有東西都不見了。

Day 6 我們用 List 存了一堆待辦事項,Day 7 把它包成 Function。但只要按下 Ctrl+C,那些資料就消失了。下次打開程式,又是一張白紙。

對一個「生活助理」來說,這是致命的。使用者今天告訴它「我早上七點起床」,明天它就忘了,那它永遠只是一個問答機器,不是助理。

今天要做的,就是讓資料離開記憶體,寫進硬碟


一、記憶體 vs 硬碟

先把概念講清楚:

記憶體(RAM) 硬碟(檔案)
速度
程式關掉後 消失 留著
在 Python 裡 變數、List、Dictionary 檔案

所以「儲存資料」這件事,本質上就是:把記憶體裡的變數,用某種格式寫成檔案;下次啟動時再讀回來。


二、開檔案的三種模式

Python 用 open() 開檔案,最常用的三種模式:

open("notes.txt", "r")   # read   讀取(檔案必須存在)
open("notes.txt", "w")   # write  寫入(會清空原內容!)
open("notes.txt", "a")   # append 附加(接在原內容後面)

⚠️ "w" 是一個很容易踩雷的模式——它會把原本的檔案內容整個清空。如果你只是想加一行,請用 "a"

另外,處理中文時請一定要加上 encoding="utf-8",尤其在 Windows 上,不加常常會變成亂碼或直接報錯:

open("notes.txt", "r", encoding="utf-8")

這個坑我自己踩過,特別標出來。


三、寫入檔案

最基本的寫法:

f = open("notes.txt", "w", encoding="utf-8")
f.write("今天學了檔案讀寫\n")
f.write("明天要學 JSON\n")
f.close()

注意兩件事:

  1. write() 不會自動換行,要自己加 \n
  2. 一定要 close(),不然資料可能還卡在緩衝區沒真正寫進去。

但手動 close() 很容易忘記,而且如果中間出錯(回想昨天的內容),close() 就不會被執行。所以 Python 提供了更好的寫法:

with 陳述式(推薦)

with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("今天學了檔案讀寫\n")
    f.write("明天要學 JSON\n")
# 離開 with 區塊時,Python 自動幫你 close()

with 的意思是「在這個區塊裡使用這個資源,離開時自動收拾」。就算區塊裡面出錯,檔案一樣會被正確關閉——它內建了昨天講的 finally 精神。

以後開檔案,一律用 with,不要再手動 close() 了。

一次寫多行

lines = ["買牛奶\n", "運動 30 分鐘\n", "寫鐵人賽文章\n"]

with open("todos.txt", "w", encoding="utf-8") as f:
    f.writelines(lines)

或者用迴圈(我自己比較常用這種,因為比較好控制格式):

todos = ["買牛奶", "運動 30 分鐘", "寫鐵人賽文章"]

with open("todos.txt", "w", encoding="utf-8") as f:
    for todo in todos:
        f.write(f"{todo}\n")

四、讀取檔案

讀取有三種常見方式:

# 方式 1:整個檔案讀成一個字串
with open("todos.txt", "r", encoding="utf-8") as f:
    content = f.read()
print(content)

# 方式 2:讀成一個 List,每行一個元素
with open("todos.txt", "r", encoding="utf-8") as f:
    lines = f.readlines()
print(lines)
# ['買牛奶\n', '運動 30 分鐘\n', '寫鐵人賽文章\n']

# 方式 3:一行一行讀(檔案很大時用這個,最省記憶體)
with open("todos.txt", "r", encoding="utf-8") as f:
    for line in f:
        print(line.strip())    # strip() 去掉頭尾的空白和換行

方式 3 是最推薦的——它不會一次把整個檔案塞進記憶體,而且搭配 Day 5 的迴圈觀念很自然。

別忘了 strip()

從檔案讀出來的每一行結尾都會帶著 \n,如果直接拿去比對,會很困惑:

line = "買牛奶\n"
print(line == "買牛奶")          # False ← 為什麼!
print(line.strip() == "買牛奶")  # True

.strip() 去掉頭尾空白字元,.rstrip() 只去掉右邊。這是新手最常卡住的地方之一。


五、檔案不存在怎麼辦?

昨天學的例外處理,今天就派上用場了:

def load_todos(path):
    """從檔案讀取待辦清單。檔案不存在時回傳空清單。"""
    try:
        with open(path, "r", encoding="utf-8") as f:
            return [line.strip() for line in f if line.strip()]
    except FileNotFoundError:
        return []


todos = load_todos("todos.txt")
print(f"載入 {len(todos)} 筆待辦")

這個設計很重要:第一次執行程式時檔案一定不存在。與其讓程式崩潰,不如把「檔案不存在」解讀成「還沒有任何資料」,回傳一個空清單。

這裡也順便用到了 Day 6 的 List 綜合表達式(list comprehension)

[line.strip() for line in f if line.strip()]

意思是:把檔案的每一行去掉空白,而且只留下非空的行。


六、用 pathlib 處理路徑

字串拼路徑在 Windows 和 Mac / Linux 上會不一樣(\ vs /),很容易出錯。Python 內建的 pathlib 幫你解決這件事:

from pathlib import Path

# 建立路徑物件
data_dir = Path("data")
todo_file = data_dir / "todos.txt"      # 用 / 串接,跨平台都正確

# 常用操作
data_dir.mkdir(exist_ok=True)           # 建立資料夾,已存在就不報錯
print(todo_file.exists())               # 檔案是否存在
print(todo_file.suffix)                 # .txt
print(todo_file.stem)                   # todos

# 直接讀寫(超方便,不用 with)
todo_file.write_text("買牛奶\n運動\n", encoding="utf-8")
content = todo_file.read_text(encoding="utf-8")

Path.write_text()read_text() 適合小檔案,一行搞定。大檔案還是用 with open() 逐行處理比較好。

mkdir(exist_ok=True) 這個參數也很實用——確保資料夾存在,但已經存在時不要報錯。


七、實作:一個會記住東西的待辦程式

把今天學的東西整合起來,做一個真的能用的小工具:

from pathlib import Path

DATA_DIR = Path("data")
TODO_FILE = DATA_DIR / "todos.txt"


def load_todos():
    """讀取待辦清單。"""
    if not TODO_FILE.exists():
        return []
    with open(TODO_FILE, "r", encoding="utf-8") as f:
        return [line.strip() for line in f if line.strip()]


def save_todos(todos):
    """把待辦清單寫回檔案。"""
    DATA_DIR.mkdir(exist_ok=True)
    with open(TODO_FILE, "w", encoding="utf-8") as f:
        for todo in todos:
            f.write(f"{todo}\n")


def add_todo(title):
    """新增一筆待辦。"""
    if not title.strip():
        raise ValueError("標題不能是空的")
    todos = load_todos()
    todos.append(title.strip())
    save_todos(todos)
    return todos


def main():
    print("=== 待辦小幫手 ===")
    print("指令:add <內容> / list / quit\n")

    while True:
        command = input("> ").strip()

        if command == "quit":
            print("掰掰")
            break

        elif command == "list":
            todos = load_todos()
            if not todos:
                print("(目前沒有待辦)")
            for i, todo in enumerate(todos, start=1):
                print(f"{i}. {todo}")

        elif command.startswith("add "):
            title = command[4:]
            try:
                todos = add_todo(title)
                print(f"已新增,目前共 {len(todos)} 筆")
            except ValueError as e:
                print(f"新增失敗:{e}")

        else:
            print("看不懂這個指令")


if __name__ == "__main__":
    main()

執行看看:

=== 待辦小幫手 ===
指令:add <內容> / list / quit

> add 買牛奶
已新增,目前共 1 筆
> add 寫鐵人賽文章
已新增,目前共 2 筆
> list
1. 買牛奶
2. 寫鐵人賽文章
> quit
掰掰

關掉程式,再重新執行一次,輸入 list

> list
1. 買牛奶
2. 寫鐵人賽文章

資料還在。這是今天最重要的一刻——我們的程式第一次有了「記憶」。


八、這跟 Agent 有什麼關係?

檔案讀寫看起來很基礎,但它其實是 Agent 架構裡好幾個關鍵功能的底層:

1. 長期記憶(Long-term memory)

Agent 需要記住使用者的偏好:「我不吃牛肉」、「我習慣晚上運動」、「提醒我請用中文」。這些東西不能每次對話都重講一次,必須存在某個地方,下次啟動時讀回來。

2. 對話歷史(Conversation history)

多輪對話中,Agent 要記得前面講過什麼。如果我們希望「今天的對話明天還記得」,就要把對話存檔。

3. 執行日誌(Log)

Agent 做了哪些決定、呼叫了哪些工具、結果如何——這些要寫下來,才能事後檢查它為什麼做了某個決定。(Day 17 會專門講這個)

4. 快取(Cache)

天氣 API 十分鐘內問三次,答案都一樣,沒必要真的打三次。把結果存成檔案,設一個有效期限,就省下了時間和 API 額度。

今天寫的 load_todos() / save_todos() 這組函式,結構上跟真正的 Agent 記憶模組是一樣的:一個負責讀進來,一個負責寫回去,中間的程式碼只管操作記憶體裡的資料。

不過用純文字檔有個大問題:結構化資料存不進去

我們的待辦事項如果不只有標題,還有優先度、完成狀態、建立時間,用 txt 要怎麼存?用逗號分隔嗎?那內容本身有逗號怎麼辦?

這就是明天要講的 JSON 要解決的問題。


小結

  • with open(...) as f: 開檔案,會自動幫你關檔,就算出錯也一樣
  • 處理中文一定要加 encoding="utf-8"
  • "w" 會清空檔案,想附加請用 "a"
  • 讀出來的每一行記得 .strip()
  • 檔案不存在時用 try/except FileNotFoundError 回傳空資料,不要讓程式崩潰
  • pathlib.Path 讓路徑處理跨平台又好讀

明天我們要讓資料「有結構」:JSON 與 Agent 的共通語言


上一篇
讓程式在出錯時不會停下來:例外處理與 Agent 的穩定性
系列文
從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言